package littlecyberwar.tool;

import javax.swing.*;
import java.awt.*;
import java.awt.geom.*;
import java.awt.event.*;
import java.util.*;
import java.text.DecimalFormat;

import littlecyberwar.model.*;
import littlecyberwar.ui.*;
import littlecyberwar.util.*;

import ModuleBuilder.model.*;

/**
 * @author Milton  Soong
 *
 * To change the template for this generated type comment go to
 * Window&gt;Preferences&gt;Java&gt;Code Generation&gt;Code and Comments
 */
/**
 * This is the super class which all Handlers inherits from
 */
public abstract class MouseHandler {
  InternalCmdFacade model;
  
  /**
   * Make the current element into an array
   */
  ArrayList selectedElements;
  
  Element currEl;
  Shape selectedRectangle;
  Shape originalRec;
  Shape oldFinalShape;

  /**
   * This is the original location of the element
   */
  AffineTransform selectedTrans;

  /**
   * This is the ongoing location of the element
   */
  AffineTransform currTrans;

  String mapDistanceUnit;
  double mapScale;
  double pixMoved;  // amount moved during the current operaton
  static JLabel sideView;
  static CardLayout profileCard;
  static ProfileCardPanel profilePanel;

  /**
   * in selected shape's user space. (The screen coordinate has been
   * inversely transformed back to shape's user space.
   */
  Point startDragP;

  /**
   * The endDragP is also inversely transformed into the shape's space
   * this is updated as the mouse drags across the screen.
   */
  Point2D endDragP;
  PlayingBoardPanel comp;

  /**
   * The DecimalFormat is used to format the amount of distance moved
   */
  public static DecimalFormat df = new DecimalFormat("0.#");
  
  protected String unitSelectedText ="default unitSelectedText".intern();
  protected String noUnitSelectedText = "default noUnitSelectedText".intern();

  /**
   * Each Handler will have an ID that'll be used as a shorthand to indicate
   * The kind of operation required.
   */
  Integer id = new Integer(-1);

  /**
   * A handler ID
   */
  public final static Integer H_UNDO = new Integer(0);
  public final static Integer H_BACKWARD = new Integer(1);
  public final static Integer H_FREEFORM = new Integer(2);
  public final static Integer H_OBLIQUE = new Integer(3);
  public final static Integer H_PLACEMENT = new Integer(4);
  public final static Integer H_ROTATECENTER = new Integer(5);
  public final static Integer H_ROTATELEFT = new Integer(6);
  public final static Integer H_ROTATERIGHT = new Integer(7);
  public final static Integer H_RULER = new Integer(8);
  public final static Integer H_SIDESTEP = new Integer(9);
  public final static Integer H_STRAIGHTFORWARD = new Integer(10);
  public final static Integer H_WATCH = new Integer(11);
  public final static Integer H_ALIGNFACING = new Integer(12);
  public final static Integer H_ALIGNVERTICAL = new Integer(13);
  public final static Integer H_ALIGNHORIZONTAL = new Integer(14);
  public final static Integer H_REMOVE = new Integer(15);
  
  public static HashMap actionStringLookup = new HashMap(); 
  
  static { 
  	actionStringLookup.put(H_BACKWARD, " moved backward ");
	actionStringLookup.put(H_FREEFORM, " freeform moved ");
	actionStringLookup.put(H_OBLIQUE, " moved obliquely ");
	actionStringLookup.put(H_PLACEMENT, " just been placed on the board.");
	actionStringLookup.put(H_ROTATECENTER, " rotated around its center ");
	actionStringLookup.put(H_ROTATELEFT, " rotated left ");
	actionStringLookup.put(H_ROTATERIGHT, " rotated right ");
	actionStringLookup.put(H_SIDESTEP, " side stepped ");
	actionStringLookup.put(H_STRAIGHTFORWARD, " moved straight ");
	actionStringLookup.put(H_WATCH, " watch ");
	actionStringLookup.put(H_ALIGNFACING, " align facing ");
	actionStringLookup.put(H_ALIGNVERTICAL, " align vertical ");
	actionStringLookup.put(H_ALIGNHORIZONTAL, " align horizontal ");
	actionStringLookup.put(H_REMOVE, " remove ");
  }
  
  private Stroke outlineStroke = new BasicStroke(PlayingBoardPanel._DEFAULT_STROKE_WIDTH, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, PlayingBoardPanel._STROKE_WIDTH);
  
  SelectionState selectionState;

  Point dragSelectStartPoint;
  
  /**
   * The selection rectangle
   *
   */
  Rectangle selectionRectangle;

  public MouseHandler() {
	selectedElements = new ArrayList(5);
	selectionState = new SelectionState();
  }

	public class SelectionState {
		// see state description on notes page.61
		public static final int IDLE=1;
		public static final int ELEMENTS_SELECTED=2;
		public static final int START_DRAG_SELECT=3;
		
		private int currState;
		
		public SelectionState() {
			currState = IDLE;
		}
		
		// from here onward all calls are inputs into the state machine
		public void click_space() {
			switch ( currState ){
				case IDLE:
					// in this case do nothing
					break;
					
				case ELEMENTS_SELECTED:
					// once you click space, it's over, reset everything
					resetSelectedElements();		
					currState = IDLE;
					break;
					
				case START_DRAG_SELECT:
					// illegal state, should've seen a mouseUp first
					Debug.out("Ilegal state: START_DRAG_SELECT meets click_space");
					break;	
			}
		}
		
		public void click_element(MouseEvent e, Element clickedElement){
			switch ( currState ){
				case IDLE:
				case ELEMENTS_SELECTED:
					// one and only one element is selected, so 
					// make it the only selected element, and then change state
					//
					oneElementSelected(clickedElement); 
					currState = ELEMENTS_SELECTED;
					break;
					
				case START_DRAG_SELECT:
					// illegal state, should've seen a mouseUp first
					Debug.out("Ilegal state: START_DRAG_SELECT meets click_element");
					break;	
			}
		}
		
		public void shift_click_element(MouseEvent e, Element clickedElement){
			switch ( currState ){
				case IDLE:
					// add the selected element to the list						
					addElementToSelected(clickedElement); 
					currState = ELEMENTS_SELECTED;
					break;

				case ELEMENTS_SELECTED:
					// first check if already selected,
					// if not add it, if so, bring it to the FIRST element
					boolean alreadySelected = checkIfAlreadySelected(clickedElement);
					if ( alreadySelected == true ){
						
					} else {
						addElementToSelected(clickedElement);
					}						 
					currState = ELEMENTS_SELECTED;
					break;
				
				case START_DRAG_SELECT:
					// illegal state, should've seen a mouseUp first
					Debug.out("Ilegal state: START_DRAG_SELECT meets shift_click_element");
					break;	
			}
		}
		
		public void mouse_down(MouseEvent e) {
			switch ( currState ){
				case IDLE:
					// now it's time to see if we are drag selecting a bunch of units
					dragSelectStartPoint = e.getPoint();
					currState = START_DRAG_SELECT;
					break;

				case ELEMENTS_SELECTED:
					// since no shift button is pressed, reset all of the already selected 
					// elements and then start a drag select process
					resetSelectedElements();
					dragSelectStartPoint = e.getPoint();
					currState = START_DRAG_SELECT;					
					break;
				
				case START_DRAG_SELECT:
					// illegal state, should've seen a mouseUp first
					Debug.out("Ilegal state: START_DRAG_SELECT meets mouse_down");
					break;	
			}

		}
		
		public void shift_mouse_down(MouseEvent e) {
			switch ( currState ){
				case IDLE:
					// now it's time to see if we are drag selecting a bunch of units
					dragSelectStartPoint = e.getPoint();
					currState = START_DRAG_SELECT;
					break;

				case ELEMENTS_SELECTED:
					// since no shift button is pressed, reset all of the already selected 
					// elements and then start a drag select process
					resetSelectedElements();
					dragSelectStartPoint = e.getPoint();
					currState = START_DRAG_SELECT;					
					break;
				
				case START_DRAG_SELECT:
					// illegal state, should've seen a mouseUp first
					Debug.out("Ilegal state: START_DRAG_SELECT meets mouse_down");
					break;	
			}
		}
		
		public void mouse_up(MouseEvent e) {
			switch ( currState ){
				case IDLE:
					// who cares
					break;

				case ELEMENTS_SELECTED:
					// illegal state
					Debug.out("Ilegal state: ELEMENTS_SELECTED meets mouse_up");					
					break;
				
				case START_DRAG_SELECT:
					/**
					 * the dragSelect rectangle is now determined, 
					 * Find all the elements included in the rectangle, and Add
					 * all of them to the selected element list
					 */
					processDragSelectRectangle(e);
					if ( selectedElements.isEmpty()){
						currState = IDLE;
					} else {
						currState = ELEMENTS_SELECTED;
					}
					break;	
			}

		}
		
		public void mouse_drag(MouseEvent e){
			switch ( currState ){
				case IDLE:
					// illegal state, should have a mouse down first
					Debug.out("Ilegal state: IDLE meets drag");					
					break;

				case ELEMENTS_SELECTED:
					// do the mouseDragged action
					mouseDragged(e);					
					break;
				
				case START_DRAG_SELECT:
					/**
					 * Update the drag select rectangle
					 */
					updateDragSelectRectangle(e);
					break;	
			}
			
		}
		
	} //public class SelectionState

	/**
	 * 1. get the current list of selected elements, build a bounds rectangle
	 * 2. clear the arraylist
	 * 3. repaint the bounds rectangle (to "erase" the selected rectangles)
	 */	
	public void resetSelectedElements() {
		Area area = null;
		for ( int i = 0; i < selectedElements.size(); i++){
			Element el = (Element)selectedElements.get(i);
			// area.add(new Area(el.getRectangle().getBounds()));	
			DirtyAreaManager.addDirtyArea(el.getRectangle());
		}
		// now the area contains the sum of all the shapes
		selectedElements.clear();
//		if ( area != null ){
//			Rectangle rec = area.getBounds();
//			comp.repaint(rec);
//		}
		DirtyAreaManager.checkAndRepaint(comp);
		return;
	}
	
	/**
	 * One and only one element selected, make it so
	 * @param clickedElement
	 */
	public void oneElementSelected(Element clickedElement){
		resetSelectedElements();
		selectedElements.add(0, clickedElement);
		repaintSelectedArea();
		return;		
	}
	
	/**
	 * 1. Add this to the existing list of selected element
	 * 2. repaint the area to include the latest addition
	 * 
	 * @param clickedElement
	 */
	public void addElementToSelected(Element clickedElement){
		selectedElements.add(0, clickedElement);
//		Rectangle rec = clickedElement.getRectangle().getBounds();
//		comp.repaint(rec);
		
		DirtyAreaManager.addDirtyArea(clickedElement.getRectangle());
		DirtyAreaManager.checkAndRepaint(comp);
		return;
	}
	
	/**
	 * Loop through the selectedElements and see if already present
	 * 
	 * @param clickedElement
	 * @return true if already in the selectedElements arraylist
	 */
	public boolean checkIfAlreadySelected(Element clickedElement){
		boolean val = false;
		for ( int i = 0; i < selectedElements.size(); i++){
			Element el = (Element)selectedElements.get(i);
			if ( clickedElement == el ){
				val = true;
				break;			
			}
		}
		return ( val );
	}
	
	/**
	 * The user is now in the process of dragging the selection rectangle
	 * 1. update the rectangle 
	 * 3. repaint the rectangle area.
	 * 
	 * @param endDragSelectPoint
	 */
	public void updateDragSelectRectangle( MouseEvent endDragSelectPoint ){
		int x = endDragSelectPoint.getX() - (int)dragSelectStartPoint.getX();
		int y = endDragSelectPoint.getY() - (int)dragSelectStartPoint.getX();
		Dimension d = new Dimension(x, y);
		selectionRectangle = new Rectangle(dragSelectStartPoint, d);
	}

	/**
	 * clean up
	 * 
	 * @param endDragSelectPoint
	 */
	public void processDragSelectRectangle( MouseEvent endDragSelectPoint ){
		// the selectionRectangle now contains the final shape
		// now go through ALL the elements out there, and check to 
		// see which ones are completely enclosed by the rectangle.
		// 1. loop thru all element rectangles
		// 2. compare if completely contained in the rectangle
		// 3. if so, add it to the selected elements
		// 4. now set selectionRectangle to NULL
		// 5. repaint the whole area.
	}

	/**
	 * Repaint all the bounds that's included in the selectedRectangles bounds
	 *	1. loop throu all the element areas, add to the total area
	 *	2. repaint
	 */
	public void repaintSelectedArea() {
		
	}
	
	/**
	 * Called by the repaint call from the gameboard to repaint just the selection rectangle
	 * @return
	 */
	public void paintSelectionRectangle(Graphics2D g2) {
		if ( selectionRectangle != null ){
			g2.setStroke( outlineStroke );
			g2.setColor(Color.red);
			g2.draw(selectionRectangle);
		}
		return;
	}

///////////////////////////////////////////////////////////////////////////////
	
  public Integer getId () {
    return ( id );
  }

  public void setBoardPanel(PlayingBoardPanel c) {
    comp = c;
    sideView = c.getSideViewLabel();
  }
  
  public static void setProfilePanelCard( CardLayout in ){
  	profileCard = in;
  }
  
  public static void setProfilePanel( ProfileCardPanel in ){
  	profilePanel = in;
  }

  public void setModelFacade(InternalCmdFacade m) {
    model = m;
    mapScale = Util.getMapScale();
    mapDistanceUnit = Util.getMapDistanceUnit();
  }

  /**
   * IMPORTANT: only to be overwritten if the placement handler is selected
   */
  public void setElement(Element el) {
  }
  
	/**
	 * This is called when the tool become selected in case a particular tool needs to do
	 * Some preprocessing (i.e. for placement tool we might already have a selected unit from the trays
	 * to be overwritten by subclass.
	 */	 
  public void onToolSelect() {
  }

/**
 * The new Multiple element select friendly processMouseEvent
 * 
 * All mouse down will come here, and then will follow what happens on 
 * the state diagram on notebook page.61 
 * 
 * @param e the mouse event corresponding to the current user action
 */
	public void processMouseDown( MouseEvent e ) {
		
	} 
	
	
  public boolean checkSelection( MouseEvent e ) {
	int onmask = InputEvent.SHIFT_DOWN_MASK;	
  	if ((e.getModifiersEx() & onmask) == onmask) {
  		// the shift key is held down, check for multi-select
  		return ( false );
  		
  	} else {
  		// the shift key is NOT held down, single select
		Point p = ScalingDelegate.getMouseEventLocation(e.getPoint());
		ArrayList v = model.getAllElements();
		for (int i=0; i< v.size(); i++) {
		  Element el = (Element)v.get(i);
		  Shape transR = el.getFinalShape();
		  if ( transR.contains(p)) {
			// the mouse click does fall inside of an element's area
			Element oldEl = currEl;
			setSelectedElement(el);	

			startAction( e );
			updateViewAfterUnitSelected(el);
        
			// Need this section here because no command is issued if No pixel is moved
			// thus we have ghost selection outline if you change selection w/o changing the units position.
			if ( currEl != oldEl ){
				// we've changed selected element, need to repaint the board
				comp.repaint();
			}
			return( true );
		  }
		}
		if ( selectedRectangle != null ) {
			resetSelectedElement();
		
		  // set the profilePanel to the null state also
		  profilePanel.noUnitSelected();
		  resetViewAfterUnitUnselected();
	  
		  comp.repaint();
		  return( true );

		} else {
		  return ( false );
		}
  	}

  }

	void setSelectedElement(Element el){
		currEl = el;
		Shape transR = el.getFinalShape();
		selectedRectangle = transR;
		oldFinalShape = selectedRectangle;
		originalRec = el.getRectangle();
		selectedTrans = el.getTransform();
		currTrans = (AffineTransform)selectedTrans.clone();
		return;
	}
	
	void resetSelectedElement(){
		startDragP = null;
		selectedRectangle = null;
		selectedTrans = null;
		oldFinalShape = null;
		//System.out.println("selectedTrans set to null...");
		currTrans = null;
		currEl = null;
	}
	
	// only do something if the selected element happens to be selected
	public void resetSelectedElement( Element el ) {
		if ( currEl == el ) {
			startDragP = null;

			selectedRectangle = null;
			selectedTrans = null;
			oldFinalShape = null;
			//System.out.println("selectedTrans set to null...");
			currTrans = null;
			currEl = null;
		}
		return;
	}
	
  // update all view with the current selected unit
  public void updateViewAfterUnitSelected(Element el){
	ImageIcon sideImage = el.getSideImage();
	sideView.setIcon(sideImage);
	// sideView.setText(currEl.getName());
	profilePanel.setUnit(currEl.getUnit());
  }
  
  public void resetViewAfterUnitUnselected() {
	ImageIcon sideImage = ControlPanel.sideViewImage;
	sideView.setIcon(sideImage);
  }
  
  public Shape getSelectedRectangle() {
    return ( selectedRectangle );
  }

  public void setSelectedRectangle(Shape s) {
    selectedRectangle = s;
  }

  public void startAction( MouseEvent e ) {
    pixMoved = 0;
    comp.printToStatus( unitSelectedText);
  }

  /**
   * mouse is released. Put the rectangle to the final currTrans
   * and also for the default case keep the selected rectangle
   * intact.
   * The amount that a unit "moves" is also printed out here.
   * At this point the linear distance between startDragP and
   * endDragP is the distance in pixels.
   * 
   * modifying code to return a unit command at this point, and do NOT
   * directly apply the change to the model or element.
   */
  public UnitCommand mouseUp( MouseEvent e ) {
  	UnitCommand cmd=null;
  	
    if ( selectedRectangle != null ) {
		double dist=0;
		if ( (startDragP != null) && (endDragP != null) ) {
			dist= startDragP.distance(endDragP);
		}
		if ( pixMoved > 0 ){			
			cmd = new UnitCommand(currEl.getElementId(), id, currTrans, pixMoved/mapScale);
			cmd.setPlayerName(Players.getSelfName());
		}    	
    }
	return ( cmd );      
  }

  public void mouseDragged( MouseEvent e ) {
    if ( (selectedTrans != null) && (selectedRectangle != null) ) {
      Rectangle r = selectedRectangle.getBounds();
      Point newP = e.getPoint();
  		Point scaledP = ScalingDelegate.getMouseEventLocation(newP);
      dragSelected(scaledP);
       //!! need to use bounds check to select just changed area
      if ( (startDragP != null) && (endDragP != null) && (pixMoved > 0) ) {
         comp.printToStatus("Unit tentatively moved: " + df.format(pixMoved/mapScale) + " " + mapDistanceUnit );
      }
      r.add(selectedRectangle.getBounds());
      r.grow(PlayingBoardPanel._DEFAULT_STROKE_WIDTH, PlayingBoardPanel._DEFAULT_STROKE_WIDTH);
     
      // now clearout the selected rectangle area
      Rectangle scaledRec = ScalingDelegate.getScaledRectangle(r);
      scaledRec.grow(PlayingBoardPanel._DEFAULT_STROKE_WIDTH, PlayingBoardPanel._DEFAULT_STROKE_WIDTH);		
      comp.repaint(scaledRec);
    }
  }

  protected void dragSelected( Point newP ) {
    // override by subclass
  }

  // whenever any of the handler has modified the state of the model,
  // need to fire this so all VIEWS gets updated
  public void modelChanged() {
    model.modelChanged();
  }

//  public void modelChanged(Rectangle r) {
//    model.modelChanged(r);
//  }

  public void setNoUnitSelectedHelpText () {
    comp.printToStatus(noUnitSelectedText);
  }

  public void setUnitSelectedHelpText() {
    comp.printToStatus(unitSelectedText);
  }

	/**
	 * Each of the tool gets called here to do its own painting of 
	 * units and other overlays
	 * 
	 * @param g the graphics2D context passed in from the main PlayingBoardPanel
	 */
	public void paintStuff(Graphics2D g2) {
		// draw the outline of the selected rectangle
		//
		Shape r = getSelectedRectangle();
		if ( r != null ) {
		  g2.setStroke( outlineStroke );
		  g2.setColor(Color.red);
		  g2.draw(r);
		}

	}
	
	public void applyRemoteChange(Element elIn) {
		// implemented in sub-class
	}

	public void applyRemoteChange(ArrayList listIn) {
		// implemented in sub-class
	}	
	
	public void mouseClicked( MouseEvent e ){
	}
	
	// this handler has just been selected
	public void handlerSelected(){
	}
}